Skip to content

Wire the blob membership gate into id serve (#173) - #3917

Open
dezren39 wants to merge 1 commit into
mainfrom
queue/2956-173-blob-gate-wire
Open

dezren39 wants to merge 1 commit into
mainfrom
queue/2956-173-blob-gate-wire

Conversation

@dezren39

@dezren39 dezren39 commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Implements #173 (queue task 2956): the blob gate decided fetches/observes from a reference index that had no producer, so it was never wired into a serving path and blobs were served to any peer.

What changed

  • blob_gate: build_reference_index (transitive HashSeq children attribution), hashseq_children, fold_group_events (group log → Worlds; world key = group create-hash hex, matching the document events' group extension), RefreshingWorlds (producer-swappable fold behind the Arc the event handler holds).
  • event_store: EventStore::document_references — applied document.put events the author has not removed, as (blob hash, group) pairs; exposed via LocalEvents.
  • serve: when the node's event store is open (the web UI path, the only time serve opens it), BlobsProtocol runs the gate handler — fetches/observes membership-checked, pushes still decided by the existing AccessPolicy. A refresh task re-folds on the group-warning cadence (60s), so a removed member loses fetch without a restart. HashSeq expansion in serve only counts a blob as a collection when all its parsed children are present in the store. Without the event store, serve keeps the previous push-only handler and logs a startup warning that the gate is inactive.
  • local: LocalNode::set_blob_gate — the embedder supplies the gate to serve() (LocalNode holds no group log/event store of its own); default behavior unchanged.
  • groups::worlds::Worlds::empty for fail-closed fallbacks.

Explicit decisions (from the decomposition note on #173)

  • Documents whose event names no group are indexed under no world = unreferenced (refused to everyone), rather than a default world.
  • commands/world.rs Replica and the world_records.rs test helper are not serving paths; left ungated, as audited.

Tests

  • New blob_gate::producer_tests: index + transitive HashSeq attribution; HashSeq parsing; group-log fold gating a fetch; refresh revoking a removed member.
  • cargo test -p id --lib blob_gate — 16 passed. Neighboring suites (event_store, documents, local_events, groups, protocol) — 250 passed.
  • Not yet covered: a live two-peer fetch against the gated BlobsProtocol, and gating non-web serve (needs serve to open the event store without the web UI — a behavior change beyond this PR). Both are called out as follow-ups on Phase 3: blob gate is not wired into serve, so blobs are still served to any peer #173.
  • Local clippy could not run (laptop clippy/rustc mismatch in the shared target dir); CI clippy is the check.

Refs #173.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

The blob gate (blob_gate::decide/blob_events) decided fetches and observes
from a reference index that had no producer, so it was never wired into a
serving path and blobs were served to any peer.

- blob_gate: build_reference_index (with transitive HashSeq children),
  hashseq_children, fold_group_events (group log -> Worlds, world key =
  group create-hash hex), RefreshingWorlds (producer-swappable fold).
- event_store: EventStore::document_references — applied, unremoved
  document.put events as (blob hash, group) pairs; LocalEvents exposes it.
- serve: when the event store is open (web UI), BlobsProtocol runs the
  gate handler (push still decided by the AccessPolicy); a refresh task
  re-folds on the group-warning cadence so removals revoke fetch. Without
  the store, serve keeps push-only handling and warns the gate is inactive.
- local: LocalNode::set_blob_gate lets an embedder supply the gate to
  serve(); default behavior unchanged.
- groups::worlds::Worlds::empty for fail-closed fallbacks.

Tests: blob_gate producer tests (index + HashSeq attribution, group-log
fold gating, refresh revoking a removed member); existing suites pass.
@dezren39
dezren39 requested a review from a team as a code owner November 1, 2026 18:27
@coderabbitai

coderabbitai Bot commented Nov 1, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 33 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 70f9d8ec-c37a-4352-a473-f64fc4d63cee
Commits

Reviewing files that changed from the base of the PR and between 33aefa7 and f62c344.

Files selected for processing: 6
  • pkgs/id/src/blob_gate.rs
  • pkgs/id/src/commands/serve.rs
  • pkgs/id/src/event_store/documents.rs
  • pkgs/id/src/groups/worlds.rs
  • pkgs/id/src/local.rs
  • pkgs/id/src/local_events.rs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-01T18:31:30.874850Z f62c344 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f62c344f2e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pkgs/id/src/blob_gate.rs
Comment on lines +177 to +179
by_world.entry(key).or_default().push(event.clone());
}
Worlds::fold(&by_world, BTreeMap::new())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve causal dependencies when partitioning the group log

When the log contains more than one group, this partition produces incomplete event graphs. GroupLog::record sets after to the heads of the entire log (groups/log.rs:123–128), so creating group B after group A makes B's create depend on an event excluded from B's partition. groups::fold::index rejects that partition with MissingDependency, and build_blob_gate consequently replaces all worlds with an empty fold, denying even authorized members every fetch. Preserve the global causal graph while deriving per-group authority, rather than passing dependency-incomplete partitions to Worlds::fold.

Useful? React with 👍 / 👎.

let Ok(bytes) = store.get_bytes(hash).await else {
continue;
};
let kids = crate::blob_gate::hashseq_children(&bytes);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require explicit collection metadata before expanding references

For a raw document whose bytes contain the 32-byte digest of another stored blob, this treats the document as a collection and grants its world access to that other blob. DocumentRef carries only media type, content hash and size; it does not designate a HashSeq. For example, sharing a binary checksum document in world B can therefore expose the contents of a private world-A blob to B's members: the all_present check succeeds precisely because the private blob is stored locally. Only expand explicitly identified collections, rather than interpreting arbitrary document contents as authorization references.

Useful? React with 👍 / 👎.

if !seen.insert(hash) {
continue;
}
let Ok(bytes) = store.get_bytes(hash).await else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound blob reads performed during gate construction

When a referenced document is large, this loads its entire contents into memory merely to determine whether it might be a HashSeq—even for ordinary raw files. Document sizes are unrestricted, and this traversal runs both before the router starts and every 60 seconds, so a sufficiently large stored document can exhaust memory during startup or kill a running server during refresh. Determine collection status from explicit metadata and use bounded reads for collection expansion instead of materializing every referenced blob.

Useful? React with 👍 / 👎.

Comment on lines +988 to +992
Some(gate) => BlobsProtocol::new(
&store_handle,
Some(crate::blob_gate::blob_events(
Arc::clone(gate),
crate::blob_gate::BlobPolicy::default(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve authorized downloads of world-records blobs

When serve enables both the web UI and worlds, this gate also intercepts the blob downloads required by the existing world-mirroring flow. ServeWorlds::open_world creates a RecordsStore using the same Docs instance and blob store; RecordsStore::reconcile writes record values with doc.set_bytes, not as event-store document.put events. Those value hashes therefore never enter this gate's reference index, so even an authorized group member receiving a valid records ticket cannot download them. WorldCommand::Mirror calls world_records::replicate, which waits for those blobs and eventually times out for a nonempty records document. Include records blobs and their authorization in the gate before applying it to the shared provider.

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant